Skip to main content

Overview

Maintaining consistent code style improves readability, reduces bugs, and makes collaboration easier. This guide covers style conventions for both Java (Spring Boot) and TypeScript (React) codebases.

Java / Spring Boot Style

General Conventions

Follow standard Spring Boot and Java conventions:
  • Indentation: 4 spaces (no tabs)
  • Line Length: Maximum 120 characters
  • Encoding: UTF-8
  • File Organization: One public class per file

Naming Conventions

Classes and Interfaces

Use PascalCase for class and interface names:
// Controllers
public class ProductoController { }
public class AuthController { }

// Services
public class ProductoServiceImpl implements IProductoService { }

// Entities
public class Producto { }
public class Categorias { }

// DTOs
public class ProductoDetalleDTO { }
public class LoginDTO { }

Methods and Variables

Use camelCase for methods and variables:
public class ProductoController {
    private final ProductoServiceImpl productoService;
    private final ProductoMapper productoMapper;

    @GetMapping
    public ResponseEntity<List<ProductoDetalleDTO>> listarTodos() {
        List<Producto> productos = productoService.obtenertodoslosproductos();
        return ResponseEntity.ok(productoMapper.toDTOlist(productos));
    }

    @PostMapping
    public ResponseEntity<ProductoDetalleDTO> crear(@RequestBody ProductoDetalleDTO dto) {
        Producto creado = productoService.crearProducto(dto);
        return ResponseEntity.status(HttpStatus.CREATED).body(productoMapper.toDTO(creado));
    }
}

Constants

Use UPPER_SNAKE_CASE for constants:
public class AppConstants {
    public static final String BASE_URL = "http://localhost:8080/api";
    public static final int MAX_PRODUCTS_PER_PAGE = 50;
    public static final String DEFAULT_CURRENCY = "EUR";
}

Controller Pattern

Follow REST conventions with proper HTTP methods and status codes:
@RestController
@RequestMapping("/api/productos")
public class ProductoController {

    private final ProductoServiceImpl productoService;
    private final ProductoMapper productoMapper;

    public ProductoController(ProductoMapper productoMapper, ProductoServiceImpl productoService) {
        this.productoMapper = productoMapper;
        this.productoService = productoService;
    }

    // GET /api/productos - List all
    @GetMapping
    public ResponseEntity<List<ProductoDetalleDTO>> listarTodos() {
        List<Producto> productos = productoService.obtenertodoslosproductos();
        return ResponseEntity.ok(productoMapper.toDTOlist(productos));
    }

    // GET /api/productos/{id} - Get by ID
    @GetMapping("/{id}")
    public ResponseEntity<ProductoDetalleDTO> obtenerPorId(@PathVariable Long id) {
        return productoService.obtenerProductoPorId(id)
                .map(p -> ResponseEntity.ok(productoMapper.toDTO(p)))
                .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/productos - Create
    @PostMapping
    public ResponseEntity<ProductoDetalleDTO> crear(@RequestBody ProductoDetalleDTO dto) {
        Producto creado = productoService.crearProducto(dto);
        return ResponseEntity.status(HttpStatus.CREATED).body(productoMapper.toDTO(creado));
    }

    // PUT /api/productos/sku/{sku} - Update
    @PutMapping("/sku/{sku}")
    public ResponseEntity<ProductoDetalleDTO> actualizar(
            @PathVariable String sku,
            @RequestBody ProductoDetalleDTO dto) {
        Producto actualizado = productoService.actualizarProducto(sku, dto);
        return ResponseEntity.ok(productoMapper.toDTO(actualizado));
    }

    // DELETE /api/productos/{id} - Delete
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> eliminar(@PathVariable Long id) {
        productoService.borrarProducto(id);
        return ResponseEntity.noContent().build();
    }
}
Use constructor injection instead of field injection for better testability and immutability.

Service Layer Pattern

Use @Service annotation and implement interfaces:
@Service
public class ProductoServiceImpl implements IProductoService {

    private final ProductoRepository productoRepository;
    private final ProductoMapper productoMapper;

    public ProductoServiceImpl(ProductoMapper productoMapper, ProductoRepository productoRepository) {
        this.productoMapper = productoMapper;
        this.productoRepository = productoRepository;
    }

    @Override
    @Transactional
    public Producto crearProducto(ProductoDetalleDTO dto) {
        if (productoRepository.existsBySku(dto.getSku())) {
            throw new IllegalArgumentException("Ya existe un producto con el SKU: " + dto.getSku());
        }
        Producto producto = productoMapper.toEntity(dto);
        return productoRepository.save(producto);
    }

    @Override
    @Transactional(readOnly = true)
    public Optional<Producto> obtenerProductoPorId(Long id) {
        return productoRepository.findById(id);
    }

    @Override
    @Transactional
    public Producto actualizarProducto(String sku, ProductoDetalleDTO dto) {
        Producto productoActualizado = productoRepository.findBySku(sku)
                .orElseThrow(() -> new RuntimeException("Producto no encontrado con Sku " + sku));

        productoMapper.updatefromEntity(dto, productoActualizado);
        return productoRepository.save(productoActualizado);
    }
}
Always use @Transactional for service methods that modify data. Use @Transactional(readOnly = true) for read-only operations to optimize performance.

Entity Pattern

Use JPA annotations without Lombok for entities (current style):
@Entity
@Table(name = "Producto")
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long producto_id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "categoria_id")
    private Categorias categoria;

    @Column(name = "nombre", length = 155, nullable = false)
    private String nombre;

    @Column(name = "sku", unique = true, nullable = false, length = 50)
    private String sku;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "cantidad", column = @Column(name = "Cantidad", nullable = false, scale = 2)),
        @AttributeOverride(name = "moneda", column = @Column(name = "Moneda", nullable = false, length = 3))
    })
    private Precio precio;

    // Default constructor
    public Producto() {
    }

    // All-args constructor
    public Producto(Categorias categoria, String nombre, String sku, Precio precio) {
        this.categoria = categoria;
        this.nombre = nombre;
        this.sku = sku;
        this.precio = precio;
    }

    // Getters
    public Long getProducto_id() {
        return producto_id;
    }

    public String getNombre() {
        return nombre;
    }

    // Setters
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    // Business methods
    public void vincularCategoria(Categorias categoria) {
        if (this.categoria == categoria) {
            return;
        }
        if (this.categoria != null) {
            this.categoria.getProductos().remove(this);
        }
        this.categoria = categoria;
        if (categoria != null && !categoria.getProductos().contains(this)) {
            categoria.getProductos().add(this);
        }
    }
}

Lombok Usage

While the project includes Lombok, it’s primarily used for DTOs and utility classes:
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenDTO {
    private String token;
}

@Data
public class ApiError {
    private int status;
    private String message;
    private LocalDateTime timestamp;
}
Lombok annotations to use:
  • @Data - Generates getters, setters, toString, equals, and hashCode
  • @NoArgsConstructor - Generates no-argument constructor
  • @AllArgsConstructor - Generates constructor with all fields
  • @Builder - Implements builder pattern

MapStruct Usage

Use MapStruct for entity-DTO mapping:
@Mapper(componentModel = "spring", uses = { CategoriaMapperResumen.class })
public interface ProductoMapper {

    @Mapping(source = "categoria", target = "categoria")
    @Mapping(source = "precio.cantidad", target = "precioCantidad")
    @Mapping(source = "precio.moneda", target = "precioMoneda")
    @Mapping(source = "dimensiones.alto", target = "dimensionesAlto")
    @Mapping(source = "dimensiones.ancho", target = "dimensionesAncho")
    @Mapping(source = "dimensiones.profundidad", target = "dimensionesProfundo")
    @Mapping(source = "imagenUrl", target = "imagen_url")
    ProductoDetalleDTO toDTO(Producto producto);

    @Mapping(target = "precio", expression = "java(new Precio(dto.getPrecioCantidad(), dto.getPrecioMoneda()))")
    @Mapping(target = "dimensiones", expression = "java(new Dimensiones(dto.getDimensionesAlto(), dto.getDimensionesAncho(), dto.getDimensionesProfundo()))")
    Producto toEntity(ProductoDetalleDTO dto);

    List<ProductoDetalleDTO> toDTOlist(List<Producto> productos);

    void updatefromEntity(ProductoDetalleDTO dto, @MappingTarget Producto producto);
}
After modifying MapStruct interfaces, always recompile the project to regenerate implementation classes:
./mvnw clean compile

Comments and Documentation

Use JavaDoc for public APIs:
/**
 * Service for managing product operations.
 * Handles CRUD operations and business logic for products.
 */
@Service
public class ProductoServiceImpl implements IProductoService {

    /**
     * Creates a new product in the system.
     *
     * @param dto the product details
     * @return the created product entity
     * @throws IllegalArgumentException if SKU already exists
     */
    @Override
    @Transactional
    public Producto crearProducto(ProductoDetalleDTO dto) {
        if (productoRepository.existsBySku(dto.getSku())) {
            throw new IllegalArgumentException("Ya existe un producto con el SKU: " + dto.getSku());
        }
        Producto producto = productoMapper.toEntity(dto);
        return productoRepository.save(producto);
    }
}

TypeScript / React Style

General Conventions

  • Indentation: 4 spaces (as configured in project)
  • Line Length: Maximum 100 characters
  • Quotes: Single quotes for strings
  • Semicolons: Required

Naming Conventions

Components

Use PascalCase for React components:
// Components
export default function ProductoCard({ producto }: Props) { }
export default function Navbar() { }

// Pages
export default function Home() { }
export default function ProductDetail() { }

Variables and Functions

Use camelCase for variables, functions, and hooks:
const [productos, setProductos] = useState<Producto[]>([]);
const [isLoading, setIsLoading] = useState(false);

function fetchProductos() {
    // ...
}

const handleSubmit = (event: FormEvent) => {
    // ...
};

Types and Interfaces

Use PascalCase for TypeScript types and interfaces:
export interface Producto {
    producto_id: number;
    sku: string;
    nombre: string;
    descripcion: string;
    precioCantidad: number;
    precioMoneda: string;
    stock: number;
    imagen_url: string;
    categoria: CategoriaResumen;
}

export interface LoginDTO {
    email: string;
    password: string;
}

export type EstadoPedido = 'PENDIENTE' | 'CONFIRMADO' | 'ENVIADO' | 'ENTREGADO' | 'CANCELADO';

Component Pattern

Use functional components with TypeScript:
import { Link } from 'react-router-dom';
import type { Producto } from '../types';
import './ProductoCard.css';

interface Props {
    producto: Producto;
}

export default function ProductoCard({ producto }: Props) {
    const precio = producto.precioCantidad?.toFixed(2) ?? '—';

    return (
        <Link to={`/productos/${producto.producto_id}`} className="producto-card">
            <div className="producto-card__img-wrap">
                <img
                    src={producto.imagen_url || 'https://placehold.co/400x300?text=Sin+imagen'}
                    alt={producto.nombre}
                    className="producto-card__img"
                    loading="lazy"
                />
                {producto.es_destacado && (
                    <span className="producto-card__badge">Destacado</span>
                )}
            </div>

            <div className="producto-card__body">
                {producto.categoria && (
                    <span className="producto-card__categoria">{producto.categoria.nombre}</span>
                )}
                <h3 className="producto-card__nombre">{producto.nombre}</h3>
                <p className="producto-card__precio">
                    {precio} <span className="producto-card__moneda">{producto.precioMoneda}</span>
                </p>
            </div>
        </Link>
    );
}

API Client Pattern

Create typed API functions:
const BASE_URL = 'http://localhost:8080/api';

function getToken(): string | null {
    return localStorage.getItem('token');
}

function authHeaders(): HeadersInit {
    const token = getToken();
    return {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
    };
}

export async function apiFetch<T>(
    path: string,
    options: RequestInit = {}
): Promise<T> {
    const res = await fetch(`${BASE_URL}${path}`, {
        ...options,
        headers: {
            ...authHeaders(),
            ...(options.headers ?? {}),
        },
    });

    if (!res.ok) {
        const error = await res.json().catch(() => ({ message: 'Error desconocido' }));
        throw new Error(error.message ?? `Error ${res.status}`);
    }

    return res.json() as Promise<T>;
}

CSS Class Naming

Use BEM (Block Element Modifier) convention:
/* Block */
.producto-card { }

/* Elements */
.producto-card__img-wrap { }
.producto-card__img { }
.producto-card__body { }
.producto-card__nombre { }
.producto-card__precio { }

/* Modifiers */
.producto-card__badge--agotado { }
.producto-card--featured { }

Comments

Use JSDoc for exported functions and complex logic:
/**
 * Fetches all products from the API.
 * @returns Promise resolving to array of products
 * @throws Error if request fails
 */
export async function fetchProductos(): Promise<Producto[]> {
    return apiFetch<Producto[]>('/productos');
}

/**
 * Calculates the total price of cart items.
 * @param items - Array of cart items
 * @returns Total price including all items
 */
function calculateTotal(items: CartItem[]): number {
    return items.reduce((sum, item) => sum + (item.producto.precioCantidad * item.cantidad), 0);
}

ESLint Configuration

The project uses ESLint for code quality. Run linting:
npm run lint
Key rules:
  • No unused variables
  • Consistent spacing and indentation
  • Proper React hooks usage
  • TypeScript type safety

Code Formatting

Java Formatting

Use your IDE’s auto-formatting (Ctrl+Alt+L in IntelliJ):
  • 4 spaces indentation
  • Opening braces on same line
  • One statement per line

TypeScript Formatting

Ensure consistent formatting:
  • 4 spaces indentation
  • Single quotes
  • Trailing commas in objects/arrays
  • Semicolons required

Best Practices

Java Best Practices:
  • Use constructor injection over field injection
  • Mark service methods with @Transactional
  • Use Optional for methods that may return null
  • Validate input in service layer
  • Use specific exception types
TypeScript Best Practices:
  • Always define TypeScript interfaces for data structures
  • Use optional chaining (?.) and nullish coalescing (??)
  • Prefer const over let
  • Extract complex logic into custom hooks
  • Use semantic HTML elements

Additional Resources